/*
 *     File: TestSet.cpp
 *
 *  Purpose: a test set framework for unit testing
 *  Version: 1.0      TWI 20070311
 *           - Initial version
 *  Comments on v1.1: Added expected signal handling
 *  Version: 1.1.1    AKK 20070322
 *             - Added psignal in TestSet_handle_signal() to name the (unexpected) signal
 *             + Alternatives are: strsignal or sys_siglist[]
 *             + Voor Dev-Cpp is een extra 'extern' nodig omdat psignal
 *             + daar eigenlijk niet bestaat
 *  Version: 1.1.2    TWI 20070323
 *           - Changed all tabs to spaces
 *           - Made versioning comments consistent
 *  Version: 1.2      TWI 20080603
 *           - added support for testcase and testset setup (setup once)
 */


/*
 *  System Includes
 */


#include <cstdio>
#include <csetjmp>
#include <csignal>
#include <cstdlib>
#include <exception>

#include "devcpp.h"


/*
 *  Module includes
 */
#include "trace.hpp"
#include "TestSet.hpp"
#include "TestSet_output.hpp"

/*
 *  Module defines
 */

#ifndef __PRETTY_FUNC__
#define __PRETTY_FUNC__ "TestSet"
#endif

#define TESTSET_ENSURE(condition)                                            \
	if ( !(condition) )                                                      \
	{                                                                        \
		TestSet_output_ensure( __FILE__, __LINE__,                           \
				__PRETTY_FUNC__, #condition );                               \
				exit( __LINE__ );                                            \
	}

#define TESTSET_MAGIC(testset)                                               \
	if ( testset->magic != TEST_SET_MAGIC )                                  \
	{                                                                        \
		TestSet_output_magic( __FILE__, __LINE__,   __PRETTY_FUNC__,         \
				"TestSet", testset->magic, TEST_SET_MAGIC );                 \
				exit( __LINE__ );                                            \
	}

#define TESTCASE_MAGIC( testcase )                                           \
	if ( testcase->magic != TEST_CASE_MAGIC )                                \
	{                                                                        \
		TestSet_output_magic( __FILE__, __LINE__,   __PRETTY_FUNC__,         \
				"TestSet", testcase->magic, TEST_CASE_MAGIC );               \
				exit( __LINE__ );                                            \
	}


/*
 *  Module type definitions
 */
#ifndef __cplusplus
typedef int bool;
#endif

/*
 *  Module global data
 */

/*
 *  Module local data
 */
static int passed          = 0;
static int failed          = 0;
static int total           = 0;
static int nr_of_test_sets = 0;

static jmp_buf environment_buffer;

static TestSet default_test_set    =
{ TEST_SET_MAGIC,      "default",  0,  0,  0,  0, false, false, 0, 0, 0, 0, 0, 0, 0, 0 };
static TestSet* current_test_set   = &default_test_set;
static TestSet destructed_test_set =
{ TEST_SET_DESTRUCTED, "",        -1, -1, -1, -1, false, false, 0, 0, 0, 0, 0, 0, 0, 0 };

static int expected_signal = 0;
static int caught_signal   = 0;

/*
 *  Module local functions
 */

/*#----------------------------------------------------------------------#*/
static bool TestSet_setup(bool& show_after)
/*
 *  Description : Execute the necessary setup(_once) functions
 *
 *  Input(s)    : previous result
 *  Output(s)   : result, 
 *  Returns     : if the setup was successful
 *  Notes       : -
 *  Pre.Cond.   : signal handler should be installed (not checked!)
 *  Post.Cond.  : the current test cases can be executed
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	TESTSET_ENSURE( current_test_set                != 0 );
	TESTSET_MAGIC ( current_test_set                     );
	TESTSET_ENSURE( current_test_set->current       != 0 );
	TESTCASE_MAGIC( current_test_set->current            );
	TESTSET_ENSURE( current_test_set->current->test != 0 );

	bool result     = OK;

	if ( setjmp(environment_buffer) == 0 )
	{
		// call the setup_once function if defined and not yet called
		if ( current_test_set->setup_once_called ) {
			result = show_after = current_test_set->setup_once_result;
		}  else {
			if ( current_test_set->setup_once ) {
				result = current_test_set->setup_once_result
				= current_test_set->setup_once();
				if ( result != OK ) {
					TestSet_output_setup_once_failed( current_test_set->name );
					show_after = false;
				}

			} else {
				result = current_test_set->setup_once_result = true;
			}
			current_test_set->setup_once_called = true;
		}

		// call the setup function if defined
		if ( result && current_test_set->setup ) {
			result = current_test_set->setup();
			if ( result != OK ) {
				TestSet_output_setup_failed( current_test_set->current->name );
				show_after = false;
			}
		}
	}
	else
	{
		result = !OK;
	}

	return result;
}


/*#----------------------------------------------------------------------#*/
static void TestSet_teardown()
/*
 *  Description : Execute the necessary setup(_once) functions
 *
 *  Input(s)    : previous result
 *  Output(s)   : result, 
 *  Returns     : if the setup was successful
 *  Notes       : -
 *  Pre.Cond.   : signal handler should be installed (not checked!)
 *  Post.Cond.  : the current test cases can be executed
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	TESTSET_ENSURE( current_test_set                != 0 );
	TESTSET_MAGIC ( current_test_set                     );
	TESTSET_ENSURE( current_test_set->current       != 0 );
	TESTCASE_MAGIC( current_test_set->current            );
	TESTSET_ENSURE( current_test_set->current->test != 0 );

	if ( setjmp(environment_buffer) == 0 )
	{
		if ( current_test_set->teardown )
			current_test_set->teardown();

		// call the teardown once function if defined and the last test
		if( ( current_test_set->current->next == 0 ) && current_test_set->teardown_once )
			current_test_set->teardown_once();
	}
	else
	{
	}

}


/*#----------------------------------------------------------------------#*/
static void TestSet_execute_test(void)

/*
 *  Description : Execute the current test case
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available variables:
 *                - test_set            to access the currect test case
 *                - expected signal     to ease building test cases which
 *                                      generate a signal
 *                - environment buffer  the signal jump environment buffer
 *
 *  Pre.Cond.   : signal handler should be installed (not checked!)
 *  Post.Cond.  : the current test cases has be executed and the statistics
 *                have beed updated according to the result
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	TESTSET_ENSURE( current_test_set                != 0 );
	TESTSET_MAGIC ( current_test_set                     );
	TESTSET_ENSURE( current_test_set->current       != 0 );
	TESTCASE_MAGIC( current_test_set->current            );
	TESTSET_ENSURE( current_test_set->current->test != 0 );

	bool result                = !OK;
	bool show_after            = true;
	const char* test_case_name = current_test_set->current->name ?
			current_test_set->current->name : "<no name>";

	result = TestSet_setup( show_after );

	try {
		expected_signal = caught_signal = 0;
		if ( setjmp( environment_buffer ) == 0 )
		{
			if( result ) {
				TestSet_output_test_case_before( test_case_name );
				result = current_test_set->current->test();
			}

			//  if expected_signal was set, it should have be caught
			if( expected_signal != 0 )
				result = !OK;
		}
		else
		{
			TRACE( "Result="            << result          );
			TRACE( "expected signal = " << expected_signal );
			TRACE( "caught signal = "   << caught_signal   );
			//  in case of no signals, they will be both zero
			if(  (expected_signal != caught_signal) )
				result = !OK;
			else
				result =  OK;
		}
	}
	catch( std::exception except )
	{
		TestSet_output_unhandled_exception( test_case_name, except.what() );
		result = !OK;
	}
	catch( ... )
	{
		TestSet_output_unhandled_exception( test_case_name, 0 );
		result = !OK;
	}


	if( show_after )
		TestSet_output_test_case_after( test_case_name, result );

	// Finally call the teardown and teardown_once function if necessary
	// done here in case of memory leaks
	TestSet_teardown();

	current_test_set->passed += (result == OK);
	current_test_set->failed += (result != OK);
	current_test_set->total  += 1;
}

/*#----------------------------------------------------------------------#*/
static void TestSet_execute_test_set_tests(void)

/*
 *  Description : Executes the test cases
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available test_set
 *  Pre.Cond.   : signal handler should be installed (not checked!)
 *  Post.Cond.  : all test cases should be executed.
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	TESTSET_ENSURE(current_test_set != 0);
	TESTSET_MAGIC(current_test_set);
	TESTSET_ENSURE(current_test_set->current != 0);
	TESTCASE_MAGIC(current_test_set->current);
	TESTSET_ENSURE(current_test_set->current->test != 0);

	while( current_test_set->current != NULL)
	{
		TESTCASE_MAGIC(current_test_set->current);
		TESTSET_ENSURE(current_test_set->current->test != 0);

		TestSet_execute_test();

		/*
		 *  Make sure that after a test case the memory structure is in
		 */
		TESTSET_ENSURE(current_test_set != 0);
		TESTSET_MAGIC(current_test_set);
		TESTSET_ENSURE(current_test_set->current != 0);
		TESTCASE_MAGIC(current_test_set->current);
		TESTSET_ENSURE(current_test_set->current->test != 0);

		current_test_set->current = current_test_set->current->next;
	}
}



/*#----------------------------------------------------------------------#*/
static void TestSet_handle_signal(int a_signal)

/*-
 *  Description : Catches and handles signals if necessary
 *
 *  Input(s)    : signal_nr: signal which causes this handler to be invoked
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : -
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 *
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	if( expected_signal != a_signal ) {
		TestSet_output_handle_unexpected_signal( a_signal );
	}

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE(current_test_set != 0);
	TESTSET_MAGIC(current_test_set);
	if(  current_test_set->current != NULL )
	{
		TESTSET_ENSURE(current_test_set->current != 0);
		TESTCASE_MAGIC(current_test_set->current);
		TESTSET_ENSURE(current_test_set->current->test != 0);

		(void) signal( a_signal, TestSet_handle_signal );
		caught_signal = a_signal;
	}

	TRACE( "before longjump" );
	longjmp( environment_buffer, 0 );
	TRACE( "after longjump" );

	exit(a_signal);
}


/*#----------------------------------------------------------------------#*/
static void TestSet_install_handlers(void)

/*-
 *  Description : Installs the signal handlers for the required signals
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : SIGKILL can't be caught (and that is also not usefull
 *                because run away tests must remain killable)
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	int catched_signals[] = {
			1,2,3,4,5,6,7,8,/*SIGKILL,*/10,
			11,12,13,14,15,16,17,18,/*SIGSTOP,*/20,
			21,22,23,24,25,26,27,28,29,30,31,
			SIGABRT,
			SIGFPE,
			SIGILL,
			SIGSEGV,
			SIGILL,   // SIGBUS,
			SIGTERM,
			22,       // SIGABRT in devcpp
			NSIG      //SIGKILL  // Not catched, last resort when test fails recursively
	};
	int index = 0;

	/* now we can reset the signal handler, execute the next test case */
	while ( catched_signals[index] != NSIG ) //SIGKILL )
	{
		(void) signal( catched_signals[index], TestSet_handle_signal );
		index++;
	}
#ifdef GCC
	std::set_terminate ( __gnu_cxx::__verbose_terminate_handler )
#endif
}


/*#----------------------------------------------------------------------#*/
static void TestSet_execute_test_set()

/*-
 *  Description : Execute all test cases belonging to the test set
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Te test set itself contains the administration like
 *                the number of executed test cases, failed and passed etc.
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	TESTSET_ENSURE( current_test_set != 0 );
	TESTSET_MAGIC( current_test_set );

	if( current_test_set->nr_of_testcases < 1 ) {

		// the default test set can be ignored silently
		if( current_test_set != &default_test_set )
		{
			const char* test_set_name = current_test_set->name ? current_test_set->name : "<noname>";

			TestSet_output_test_set_before(test_set_name, current_test_set->nr_of_testcases);
		}
	}
	else
	{
		const char* test_set_name = current_test_set->name ?  current_test_set->name : "<noname>";
		TestSet_output_test_set_before(test_set_name, current_test_set->nr_of_testcases);
		current_test_set->current = current_test_set->head;

		// use setjmp to jump back here if anything fails
		TestSet_execute_test_set_tests();

		TestSet_output_test_set_after(test_set_name,
				current_test_set->passed,
				current_test_set->failed,
				current_test_set->total);
		nr_of_test_sets++;
	}

	passed += current_test_set->passed;
	failed += current_test_set->failed;
	total  += current_test_set->total;
}


/*
 *  Module Exported functions
 */

/*#----------------------------------------------------------------------#*/
extern TestSet* TestSet_add_set( TestSet* a_test_set )

/*
 *  Description : Adds a new test set to the test sets
 *
 *  Input(s)    : a_test_set - the test set to be added
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   :
 *  Post.Cond.  :
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE( current_test_set != 0          );
	TESTSET_ENSURE( a_test_set       != 0          );
	TESTSET_MAGIC ( current_test_set               );
	TESTSET_MAGIC ( a_test_set                     );
	TESTSET_ENSURE( current_test_set != a_test_set );

	current_test_set->next = current_test_set = a_test_set;  // default is the start
	nr_of_test_sets++;

	return 0;
}


/*#----------------------------------------------------------------------#*/
extern TestCase* TestSet_add_test_case( TestCase* a_test_case )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : a_test_case - the test case to be added
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE( current_test_set != 0 );
	TESTSET_MAGIC ( current_test_set      );
	TESTSET_ENSURE( a_test_case      != 0 );
	TESTCASE_MAGIC( a_test_case           );

	if( current_test_set->tail == 0)
	{
		current_test_set->head = current_test_set->tail = a_test_case;
	}
	else
	{
		current_test_set->tail->next = current_test_set->tail = a_test_case;
	}
	current_test_set->nr_of_testcases++;

	return NULL;
}

/*#----------------------------------------------------------------------#*/
extern TestSetupFunction TestSet_set_setup( TestSetupFunction setup_function )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : setup_function - setup function for each test in this test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE( current_test_set != 0 );
	TESTSET_MAGIC ( current_test_set      );
	TESTSET_ENSURE( setup_function   != 0 );


	return NULL;

}


/*#----------------------------------------------------------------------#*/
extern TestTeardownFunction TestSet_set_teardown( TestTeardownFunction teardown_function )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : teardown_function - teardown function for each test in this
 *                test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE( current_test_set  != 0 );
	TESTSET_MAGIC ( current_test_set       );
	TESTSET_ENSURE( teardown_function != 0 );

	TESTSET_ENSURE(current_test_set->teardown == NULL);
	current_test_set->teardown = teardown_function;

	return NULL;
}

/*#----------------------------------------------------------------------#*/
extern TestSetupFunction TestSet_set_setup_once( TestSetupFunction setup_once_function )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : setup_once_function - setup function for each test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE( current_test_set    != 0 );
	TESTSET_MAGIC ( current_test_set         );
	TESTSET_ENSURE( setup_once_function != 0 );

	TESTSET_ENSURE( current_test_set->setup_once == 0 );
	current_test_set->setup_once = setup_once_function;

	return NULL;

}


/*#----------------------------------------------------------------------#*/
extern TestTeardownFunction TestSet_set_teardown_once( TestTeardownFunction teardown_once_function )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : teardown_once_function - teardown function for each test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	/* Checks to verify that the test set memory is not corrupted */
	TESTSET_ENSURE( current_test_set       != 0 );
	TESTSET_MAGIC ( current_test_set            );
	TESTSET_ENSURE( teardown_once_function != 0 );

	TESTSET_ENSURE( current_test_set->teardown_once == NULL );
	current_test_set->teardown_once = teardown_once_function;

	return NULL;
}


/*#----------------------------------------------------------------------#*/
void TestSet_expect_signal( int a_signal )

/*-
 *  Description : Catches and handles signals if necessary
 *
 *  Input(s)    : a_signal: signal number to be generated by the test case
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : -
 *  Pre.Cond.   : the signal may not already have been set
 *  Post.Cond.  : the signal has been set
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	// output is generated in the macros because abort/assert signal combination

	// Checks to verify correct behaviour
	TESTSET_ENSURE( a_signal        >  0    );
	TESTSET_ENSURE( a_signal        <= NSIG );
	TESTSET_ENSURE( expected_signal == 0    );

	expected_signal = a_signal;
}


/*#----------------------------------------------------------------------#*/
int TestSet_execute()

/*-
 *  Description : Execute all test cases belonging to the test set
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : nr of failed test cases
 *  Notes       :
 *  Pre.Cond.   : test cases and test sets have to have been build
 *  Post.Cond.  : all test cases are run and test run statistics
 *                have been build
 */

/*#----------------------------------------------------------------------#*/
{
	TRACE_FUNC;

	TestSet* next_test_set;

	TestSet_install_handlers();

	current_test_set = &default_test_set;

	TestSet_output_all_test_sets_before( nr_of_test_sets );
	nr_of_test_sets = 0;

	while ( current_test_set != 0 )
	{
		/* use setjmp to jump back here if anything fails */
		if( setjmp( environment_buffer) == 0 )
		{
			TestSet_execute_test_set();
		}
		next_test_set     = current_test_set->next;
		*current_test_set = destructed_test_set;
		current_test_set  = next_test_set;
	}

	TestSet_output_all_test_sets_after( passed, failed, total, nr_of_test_sets );

	return failed;
}

/* End of Document */
